package aso.fighting_groups;

import sim.engine.SimState;
import sim.engine.Steppable;
import sim.engine.Stoppable;
import sim.util.Bag;
import sim.util.Int2D;

/**
 * Implements an Agent.
 */
class Agent implements Steppable {
    /**
     * UID for serialization.
     */
    private static final long serialVersionUID = -5641502098086378912L;
    /**
     * the amount of food the Agent currently carries.
     */
    private double carryingFoodAmount = 0;
    /**
     * the food consumption rate.
     */
    private double foodConsumption;
    /**
     * the group in which the Agent is.
     */
    private Group group;
    /**
     * if the agent is is a group.
     */
    private boolean inGroup = false;
    /**
     * the niceness of the agent. Currently not used. 
     */
    private double niceness;
    /**
     * the speed of movement of the agent. Currently not used (all speed is the same).
     */
    private double speed;
    /**
     * for how many iterations the agent was starving.
     */
    private int starvingIterations = 0;
    /**
     * the strength of the agent.
     */
    private double strength;
    /**
     * the team number of the agent.
     */
    private int team;

    /**
     * @param theStrength
     *            the strength
     * @param theSpeed
     *            the speed
     * @param theFoodConsumption
     *            the food consumption rate
     * @param theCarryingFoodAmount
     *            the food amount to carry
     * @param theTeam
     *            the team number
     */
    public Agent(final double theStrength, final double theSpeed, final double theFoodConsumption, final double theCarryingFoodAmount, final int theTeam) {
        this.setStrength(theStrength);
        this.setSpeed(theSpeed);
        this.setFoodConsumption(theFoodConsumption);
        this.setCarryingFoodAmount(theCarryingFoodAmount);
        this.setTeam(theTeam);
    }

    /**
     * @return the food amount to carry
     */
    public double getCarryingFoodAmount() {
        return carryingFoodAmount;
    }

    /**
     * @return the food consumption rate
     */
    public double getFoodConsumption() {
        return foodConsumption;
    }

    /**
     * @return the group
     */
    public Group getGroup() {
        return group;
    }

    /**
     * @return the niceness
     */
    public double getNiceness() {
        return niceness;
    }

    /**
     * @return the speed
     */
    public double getSpeed() {
        return speed;
    }

    /**
     * @return the starving iterations
     */
    public int getStarvingIterations() {
        return starvingIterations;
    }

    /**
     * @return the strength
     */
    public double getStrength() {
        return strength;
    }

    /**
     * @return the team number
     */
    public int getTeam() {
        return team;
    }

    /**
     * @return true if is in a group, false otherwise
     */
    public boolean isInGroup() {
        return inGroup;
    }

    /**
     * Move the Agent in the specified direction.
     * 
     * @param dirX
     *            x direction
     * @param dirY
     *            y direction
     * @param fg
     *            the simulation state
     */
    public void move(final int dirX, final int dirY, final FightingGroups fg) {
        Int2D location;
        location = fg.getAgentGrid()[getTeam()].getObjectLocation(this);
        fg.getAgentGrid()[getTeam()].setObjectLocation(this, fg.getAgentGrid()[getTeam()].stx(location.x + dirX), fg.getAgentGrid()[getTeam()].sty(location.y + dirY));
    }

    /**
     * @param theCarryingFoodAmount
     *            the carryingFoodAmount to set
     */
    public void setCarryingFoodAmount(final double theCarryingFoodAmount) {
        this.carryingFoodAmount = theCarryingFoodAmount;
    }

    /**
     * @param theFoodConsumption
     *            the food consumption rate
     */
    public void setFoodConsumption(final double theFoodConsumption) {
        foodConsumption = theFoodConsumption;
    }

    /**
     * @param theGroup
     *            new group
     */
    public void setGroup(final Group theGroup) {
        this.group = theGroup;
    }

    /**
     * @param isInGroup
     *            if is in group
     */
    public void setInGroup(final boolean isInGroup) {
        this.inGroup = isInGroup;
    }

    /**
     * @param theNiceness
     *            the niceness
     */
    public void setNiceness(final double theNiceness) {
        this.niceness = theNiceness;
    }

    /**
     * @param theSpeed
     *            the speed
     */
    public void setSpeed(final double theSpeed) {
        speed = theSpeed;
    }

    /**
     * @param theStarvingIterations
     *            the starving iterations
     */
    public void setStarvingIterations(final int theStarvingIterations) {
        this.starvingIterations = theStarvingIterations;
    }

    /**
     * @param theStrength
     *            the strength
     */
    public void setStrength(final double theStrength) {
        strength = theStrength;
    }

    /**
     * @param theTeam
     *            the new team
     */
    public void setTeam(final int theTeam) {
        team = theTeam;
    }

    @Override
    public void step(final SimState state) {
    	
    	//consume food each iteration
        setCarryingFoodAmount(getCarryingFoodAmount() - foodConsumption);
        if (getCarryingFoodAmount() < 0) {
            setCarryingFoodAmount(0);
        }
        
        //if the agent has no food, it is hungry. Number of hungry iterations is counted
        if (getCarryingFoodAmount() == 0) {
            starvingIterations++;
        } else {
            starvingIterations = 0;
        }
        
        //if the agent is in a group the rest of the agents will be done on the group level
        if (isInGroup()) {
            return;
        }

        FightingGroups fg = (FightingGroups) state;

        Int2D location;
        location = fg.getAgentGrid()[getTeam()].getObjectLocation(this);

        int myx = location.x;
        int myy = location.y;
        
        //gather the food present at the agent's location in the environment
        if (fg.getFoodGrid().field[myx][myy] > 0) {
            setCarryingFoodAmount(getCarryingFoodAmount() + fg.getFoodGrid().field[myx][myy]);
            fg.getFoodGrid().field[myx][myy] = 0;
        }

        //observe which team members and competitors are nearby
        Bag neighborsFriendly = new Bag();
        Bag neighborsEnemy = new Bag();
        for (int i = 0; i < fg.getTeamCount(); ++i) {
            if (i == getTeam()) {
                neighborsFriendly = fg.getAgentGrid()[i].getNeighborsMaxDistance(myx, myy, 1, true, null, null, null);
            } else {
                neighborsEnemy = fg.getAgentGrid()[i].getNeighborsMaxDistance(myx, myy, 1, true, null, null, null);
            }
        }

        //decide whether to join a group or not
        if (neighborsFriendly.size() > 0) { 
            Agent friend = (Agent) neighborsFriendly.get(fg.random.nextInt(neighborsFriendly.size()));
            
            //the agent will only join a group when starving, the group members are not starving, and the chance of joining will change based on how long the agent is starving
            if (friend.starvingIterations == 0 && fg.random.nextDouble() < (1.0 - 1.0 / (1.0 + starvingIterations * fg.getStarvationImportance()))) {
                if (friend.isInGroup()) {
                    friend.group.addMember(this);
                } else { // it forces the friend to create a group with this one
                    Bag friendInBag = new Bag();
                    friendInBag.add(friend);
                    Group newGroup = new Group(friendInBag);
                    friend.group = newGroup;
                    friend.setInGroup(true);
                    Stoppable stopper = fg.schedule.scheduleRepeating(newGroup);
                    newGroup.setStopper(stopper);
                    newGroup.addMember(this);
                    fg.addGroup(newGroup);
                }
                return;
            }
        }
        
        //fight with a nearby competitor, if any
        fight(fg, neighborsEnemy);

        //move to a nearby location (based on smell)
        move(fg, myx, myy);
    }

    /**
     * @param fg the simulation state
     * @param neighborsEnemy enemies that are near
     */
    private void fight(final FightingGroups fg, final Bag neighborsEnemy) {
    	//if an enemy (group) is nearby, there can be a fight. The enemy (group) is selected randomly
        if (neighborsEnemy.size() > 0) {
            Agent enemy = (Agent) neighborsEnemy.get(fg.random.nextInt(neighborsEnemy.size()));
            
            //fight the enemy's group or the individual enemy if the enemy is not in a group. The winner takes the food of the loser
            if (enemy.isInGroup()) {
                double groupStrength = enemy.group.getStrength();
                //the (group with the) highest strength has a bigger chance of winning
                if (fg.random.nextDouble() < (strength / (strength + groupStrength))) {
                    setCarryingFoodAmount(getCarryingFoodAmount() + enemy.group.giveFood());
                } else {
                    enemy.group.acceptFood(getCarryingFoodAmount());
                    setCarryingFoodAmount(0);
                }
            } else {
            	//the agent with the highest strength has a bigger chance of winning
                if (fg.random.nextDouble() < (strength / (strength + enemy.strength))) {
                    setCarryingFoodAmount(getCarryingFoodAmount() + enemy.getCarryingFoodAmount());
                    enemy.setCarryingFoodAmount(0);
                } else {
                    enemy.setCarryingFoodAmount(enemy.getCarryingFoodAmount() + carryingFoodAmount);
                    setCarryingFoodAmount(0);
                }
            }
        }
    }

    /**
     * @param fg the simulation state
     * @param myx my x coordinate
     * @param myy my y coordinate
     */
    private void move(final FightingGroups fg, final int myx, final int myy) {
        int moveToX = -1;
        int moveToY = 0;
        
        //move to a new location (neighbouring coordinate) based on smell
        for (int x = -1; x < 2; ++x) {
            for (int y = -1; y < 2; ++y) {
                if (!(x == 0 && y == 0)) {
                    int xx, yy;
                    xx = fg.getAgentGrid()[getTeam()].stx(x + myx); // torodial
                    yy = fg.getAgentGrid()[getTeam()].sty(y + myy); // torodial

                    //a neighbouring coordinate with a higher smell has a higher chance of being move to compared to lower smell
                    if (moveToX == -1 || (fg.getSmellGrid().field[xx][yy] > fg.getSmellGrid().field[moveToX][moveToY]) && fg.random.nextDouble() < (fg.getSmellGrid().field[xx][yy] / (fg.getSmellGrid().field[xx][yy] + fg.getSmellGrid().field[moveToX][moveToY])) || (fg.getSmellGrid().field[xx][yy] == fg.getSmellGrid().field[moveToX][moveToY] && fg.random.nextBoolean())) { // not uniform, but enough to break up the go-up-and-to-the-left syndrome
                        moveToX = xx;
                        moveToY = yy;
                    }
                }
            }
        }

        fg.getAgentGrid()[getTeam()].setObjectLocation(this, moveToX, moveToY);
    }
}
